package sourceFiles;

import Exceptions.XMLRidesParserException;

import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * A class responsible for all XML IO communication of ride data from\to the
 * ePark system.
 * 
 * @author LASO-Team
 *
 */
public class XMLRidesParser {
	private String path;

	/**
	 * Creates a new instance of an XMLRidesParser, all future IO operations
	 * with this parser will be directed on XML file pointed by 'path'
	 * 
	 * @param path
	 *            The file path to be used with the new XMLRidesParser instance
	 * @throws XMLRidesParserException
	 */
	public XMLRidesParser(String path) throws XMLRidesParserException {
		try {
			DocumentBuilderFactory.newInstance().newDocumentBuilder()
					.parse(path);
		} catch (IOException e) {
			throw new XMLRidesParserException(
					"A file IO error (error in reading from file)", e);
		} catch (ParserConfigurationException e) {
			throw new XMLRidesParserException(
					"A DOM parser error (error in reading from file)", e);
		} catch (SAXException e) {
			throw new XMLRidesParserException(
					"A SAX parser error (error in reading from file)", e);
		} catch (IllegalArgumentException e) {
			throw new XMLRidesParserException("Path cannot be null", e);
		}
		this.path = new String(path);
	}

	/**
	 * Reads the list of rides in the XML file pointed by 'path', and returns
	 * them in the form of a list of Rides
	 * 
	 * @return A list of Rides representing the controllers of the ride in the
	 *         ePark
	 * @throws XMLRidesParserException
	 *             In case of any errors in the opening or reading of the XML
	 *             file containing the list of rides in the park, will also
	 *             contain the cause of the specific error (use trace reader to
	 *             access)
	 */
	public LinkedList<IRide> readRidesFromFile(ICentralComputer comp)
			throws XMLRidesParserException {
		LinkedList<IRide> ridesList = new LinkedList<IRide>();
		Document doc = openXMLDoc();
		Node rideListNode = doc.getFirstChild();
		for (Node rideNode = rideListNode.getFirstChild(); rideNode != null; rideNode = rideNode
				.getNextSibling()) {
			if (isNodeValideRide(rideNode)) {
				addRideToList(ridesList, rideNode, comp);
			}
		}
		return ridesList;
	}

	/**
	 * Reads the list of rides in the XML file pointed by 'path', and returns
	 * them in the form of a list of RideEntries
	 * 
	 * @return A list of RideEntries representing the data of the ride in the
	 *         ePark
	 * @throws XMLRidesParserException
	 *             In case of any errors in the opening or reading of the XML
	 *             file containing the list of rides in the park, will also
	 *             contain the cause of the specific error (use trace reader to
	 *             access)
	 */
	public LinkedList<RideEntry> readRideEntriesFromFile()
			throws XMLRidesParserException {
		LinkedList<RideEntry> rideEntriesList = new LinkedList<RideEntry>();
		Document doc = openXMLDoc();
		Node root = doc.getFirstChild();
		for (Node node = root.getFirstChild(); node != null; node = node
				.getNextSibling()) {
			if (isNodeValideRide(node)) {
				addRideEntryToList(rideEntriesList, node);
			}
		}
		return rideEntriesList;
	}

	/**
	 * Writes the ride data to file (pointed by 'path'). Note: Ride and
	 * RideEntry should be matching, else result is not assured.
	 * 
	 * 
	 * @param newRide
	 *            The data contained in Ride to write to the file
	 * @param newRideEntry
	 *            The data contained in RideEntry to write to the file
	 * @throws XMLRidesParserException
	 *             In case of any errors in the opening, reading or writing
	 *             from\to the XML file containing the list of rides in the
	 *             park, will also contain the cause of the specific error (use
	 *             trace reader to access)
	 */
	public void writeNewRideDataToFile(IRide newRide, RideEntry newRideEntry)
			throws XMLRidesParserException {
		Document doc = openXMLDoc();
		// create new XML node for the new ride, and appends it to the rides
		// list.
		Node ridesList = doc.getFirstChild();
		Element newRideElement = doc.createElement("Ride");
		ridesList.appendChild(newRideElement);

		setRideAttributes(newRide, newRideEntry, doc, newRideElement);
		writeToFile(doc);
	}

	/**
	 * Updates an existing ride entry node with new data. Node is uniquely
	 * identified by rideID field in Ride or RideEntry. Note: Ride and RideEntry
	 * should be matching, else result is not assured.
	 * 
	 * @param updateRide
	 *            The data contained in the updated Ride to write to the file
	 * @param updateRideEntry
	 *            The data contained in the updated RideEntry to write to the
	 *            file
	 * @throws XMLRidesParserException
	 *             In case of any errors in the opening, reading or writing
	 *             from\to the XML file containing the list of rides in the
	 *             park, will also contain the cause of the specific error (use
	 *             trace reader to access)
	 */
	public void updateRideDataInFile(IRide updateRide, RideEntry updateRideEntry)
			throws XMLRidesParserException {
		removeRideFromFile(updateRide.getRideID());
		writeNewRideDataToFile(updateRide, updateRideEntry);
	}

	/**
	 * Removes the specified rideID from the ride list savedin the xml file in
	 * the path
	 * 
	 * @param rideID
	 *            The ride to remove from the rides list in the XML file at path
	 * @throws XMLRidesParserException
	 *             If for any reason the file is inaccessible or we cannot find
	 *             it or we cannot write to it
	 */
	private void removeRideFromFile(int rideID) throws XMLRidesParserException {
		Document doc = openXMLDoc();
		Node ridesListNode = doc.getFirstChild();
		boolean wasRemoved = false;
		for (Node rideNode = ridesListNode.getFirstChild(); rideNode != null; rideNode = rideNode
				.getNextSibling()) {
			if (isNodeValideRide(rideNode)) {
				Element rideElement = (Element) rideNode;
				int currentRideID = Integer.parseInt(new String(rideElement
						.getAttribute("rideID")));
				if (currentRideID == rideID) {
					ridesListNode.removeChild(rideNode);
					wasRemoved = true;
					break;
				}
			}
		}
		if (!wasRemoved) {
			throw new XMLRidesParserException(
					"Tried to remove a non-existing ride here", null);
		}
		writeToFile(doc);
	}

	/**
	 * the actual writing operation to the xml file
	 * @param doc
	 * @throws XMLRidesParserException
	 */
	private void writeToFile(Document doc) throws XMLRidesParserException {
		try {
			TransformerFactory transformerFactory = TransformerFactory
					.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(new File(path));
			transformer.transform(source, result);
		} catch (Exception e) {
			throw new XMLRidesParserException(
					"A Transformer error (error in writing to file)", e);
		}
	}

	/**
	 * Setting the different attributes in a ride in the xml file
	 * @param newRide
	 * @param newRideEntry
	 * @param doc
	 * @param rideElement
	 */
	private void setRideAttributes(IRide newRide, RideEntry newRideEntry,
			Document doc, Element rideElement) {

		// set maxCapacity attribute for new ride.
		Attr attr = doc.createAttribute("maxCapacity");
		attr.setValue(newRide.getMaxCapacity().toString());
		rideElement.setAttributeNode(attr);
		// set minAge attribute for new ride.
		attr = doc.createAttribute("minAge");
		attr.setValue(newRide.getMinimumAge().toString());
		rideElement.setAttributeNode(attr);
		// set minHeight attribute for new ride.
		attr = doc.createAttribute("minHeight");
		attr.setValue(newRide.getMinimumHeight().toString());
		rideElement.setAttributeNode(attr);
		// set isExtreme attribute for new ride.
		attr = doc.createAttribute("isExtreme");
		attr.setValue(newRideEntry.isExtreme().toString());
		rideElement.setAttributeNode(attr);
		// set price attribute for new ride.
		attr = doc.createAttribute("price");
		attr.setValue(newRideEntry.getPrice().toString());
		rideElement.setAttributeNode(attr);
		// set rideID attribute for new ride.
		attr = doc.createAttribute("rideID");
		attr.setValue(newRideEntry.getRideID().toString());
		rideElement.setAttributeNode(attr);
		// set rideName attribute for new ride.
		attr = doc.createAttribute("rideName");
		attr.setValue(newRideEntry.getRideName());
		rideElement.setAttributeNode(attr);
	}

	/**
	 * opening an xml file to write/read to/from it
	 * @return
	 * @throws XMLRidesParserException
	 */
	private Document openXMLDoc() throws XMLRidesParserException {
		try {
			DocumentBuilderFactory docFactory = DocumentBuilderFactory
					.newInstance();
			DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
			return docBuilder.parse(path);
		} catch (IOException e) {
			throw new XMLRidesParserException(
					"A file IO error (error in reading from file)", e);
		} catch (ParserConfigurationException e) {
			throw new XMLRidesParserException(
					"A DOM parser error (error in reading from file)", e);
		} catch (SAXException e) {
			throw new XMLRidesParserException(
					"A SAX parser error (error in reading from file)", e);
		}
	}

	/**
	 * @param node
	 * @return
	 */
	private boolean isNodeValideRide(Node node) {
		return node.getNodeType() == Node.ELEMENT_NODE
				&& node.getNodeName() == "Ride";
	}

	/**
	 * a single step in building a rides entries list from the xml file
	 * @param list
	 * @param rideNode
	 */
	private void addRideEntryToList(LinkedList<RideEntry> list, Node rideNode) {
		Element rideElement = (Element) rideNode;
		String rideName = new String(rideElement.getAttribute("rideName"));
		int rideID = Integer.parseInt(new String(rideElement
				.getAttribute("rideID")));
		double price = Double.parseDouble(new String(rideElement
				.getAttribute("price")));
		boolean isExtremeRide = Boolean.parseBoolean(new String(rideElement
				.getAttribute("isExtreme")));
		RideEntry newRideEntry;
		if (isExtremeRide) {
			newRideEntry = new ExtremeRideEntry(rideName, rideID, price);
		} else {
			newRideEntry = new RegularRideEntry(rideName, rideID, price);
		}
		list.add(newRideEntry);
	}

	/**
	 * a single step in building a rides list from the xml file
	 * @param list
	 * @param rideNode
	 * @param comp
	 */
	private void addRideToList(LinkedList<IRide> list, Node rideNode,
			ICentralComputer comp) {
		Element rideElement = (Element) rideNode;
		String rideName = new String(rideElement.getAttribute("rideName"));
		int rideID = Integer.parseInt(new String(rideElement
				.getAttribute("rideID")));
		int maxCapacity = Integer.parseInt(new String(rideElement
				.getAttribute("maxCapacity")));
		int minAge = Integer.parseInt(new String(rideElement
				.getAttribute("minAge")));
		int minHeight = Integer.parseInt(new String(rideElement
				.getAttribute("minHeight")));
		Ride newRide = new Ride(rideID, maxCapacity, minAge, minHeight,
				rideName, comp);
		list.add(newRide);

	}
}